﻿#if UNITY_EDITOR

using UnityEditor;
using UnityEngine;

namespace AI.Graph.Editor {


    public abstract class NodeView<T> : AbstractNodeView where T : Node {

        protected T node { get; private set; }
        internal NodeCanvas canvas { get; private set; }
        internal protected Vector2 mousePositionOnNode { get; private set; }

        internal NodeColors colors {
            get { return node.colors; }
        }

        /// <summary>
        /// The connected ai behaviour if one is selected.
        /// Can be null.
        /// </summary>
        protected AIBehaviour aiBehaviour { get; private set; }


        internal sealed override void Initialize (NodeCanvas canvas, AIBehaviour aiBehaviour) {
            node = target as T;
            this.canvas = canvas;
            this.aiBehaviour = aiBehaviour;

            OnViewCreated ();
        }


        internal sealed override void OnEnable () {
            if (target != null) {
                node = target as T;
                OnInspectorEnable ();
            }
        }


        /// <summary>
        /// Is called if Unity creates this Editor.
        /// And the view was created for th graph.
        /// Use this function instead of [OnEnable ()].
        /// </summary>
        protected virtual void OnInspectorEnable () { }


        /// <summary>
        /// Is called if the view was created for the graph.
        /// </summary>
        protected virtual void OnViewCreated () { }


        internal sealed override void DrawOnScene () {
            OnSceneGUI ();
            if (canvas.IsNodeSelected (node)) {
                OnSceneGUISelected ();
            }
        }


        /// <summary>
        /// Draw on the scene view
        /// </summary>
        protected virtual void OnSceneGUI () { }


        /// <summary>
        /// Draw on the scene view. Only called if the node is selected.
        /// </summary>
        protected virtual void OnSceneGUISelected () { }


        public override void OnInspectorGUI () {
            DrawDefaultNode ();
        }


        internal sealed override void DrawNode (bool drawSimple) {
            GUI.BeginGroup (node.rect);
            mousePositionOnNode = Event.current.mousePosition + node.position;

            serializedObject.Update ();

            Vector2 nodeSize = OnDrawNode (drawSimple);

            serializedObject.ApplyModifiedProperties ();

            GUI.EndGroup ();

            node.size = nodeSize;

            if (canvas.IsNodeSelected (node)) {
                DrawOutline (colors.primaryColor1);
            }
            else if (canvas.IsNodeHovered (node)) {
                DrawOutline (colors.primaryColor2);
            }
        }


        /// <summary>
        /// Draws the node and returns the size.
        /// </summary>
        /// <returns>Returns the size of the node.</returns>
        protected abstract Vector2 OnDrawNode (bool drawSimple);


        /// <summary>
        /// Draw the built-in node.
        /// </summary>
        protected bool DrawDefaultNode () {
            UnityEditor.EditorGUI.BeginChangeCheck ();
            serializedObject.Update ();
            SerializedProperty property = serializedObject.GetIterator ();
            for (bool enterChildren = true; property.NextVisible (enterChildren); enterChildren = false) {
                if (property.propertyPath == "m_Script") {
                    continue;
                }

                EditorGUILayout.PropertyField (property, true);
            }

            serializedObject.ApplyModifiedProperties ();
            return UnityEditor.EditorGUI.EndChangeCheck ();
        }


        internal override EdgePoint GetEdgePointForPort (ConnectionPort port) {
            Vector2 direction = port.direction == Direction.Input ? Vector2.left : Vector2.right;
            Color normalColor = UnityEditorInternal.InternalEditorUtility.HasPro () ? colors.primaryColor2 : colors.primaryColor2;
            Color highlightColor = UnityEditorInternal.InternalEditorUtility.HasPro () ? colors.primaryColor1 : colors.primaryColor1;
            return new EdgePoint (port.rect.center, direction, normalColor, highlightColor);
        }


        void DrawOutline (Color color) {
            EditorGUI.DrawSolidTexture (new Rect (node.position - new Vector2 (Styles.outlinePadding, Styles.outlinePadding), node.size + new Vector2 (2 * Styles.outlinePadding, 2 * Styles.outlinePadding)), color, Styles.outlineBorderWidth, Styles.outlineBorderRadius);
        }


        /// <summary>
        /// Draws a node header with the node name.
        /// </summary>
        /// <param name="y">The start point.</param>
        /// <returns>Returns the size of the draw area.</returns>
        protected internal Rect DrawNodeHeader (float y) {
            Rect topRect = new Rect (0, y, node.size.x, Styles.headerHeight);

            if (!Event.current.IsRepaintEvent ()) {
                return topRect;
            }

            BorderRadius radius = new BorderRadius (Styles.nodeBorderRadius.topLeft, Styles.nodeBorderRadius.topRight, 0, 0);

            EditorGUI.DrawSolidTexture (topRect, colors.primaryColor3, radius);

            EditorGUI.Label (Styles.nodeLabelPaddingLeft, y, topRect.width - Styles.nodeLabelPaddingLeft, topRect.height, node.name, Styles.nodeLabelStyle);

            EditorGUI.DrawSolidTexture (topRect, Styles.outlineColor.color, new BorderWidth (Styles.outlineWidth, Styles.outlineWidth, Styles.outlineWidth, 0), radius);

            return topRect;
        }


        /// <summary>
        /// Draws a node header with the node name and a toggle button.
        /// </summary>
        /// <param name="y">The start point.</param>
        /// <param name="isCollapsed">This value will be changed if the toggle button was clicked.</param>
        /// <returns>Returns the size of the draw area.</returns>
        protected internal Rect DrawNodeHeaderWithToggle (float y, ref bool isCollapsed) {
            Rect rect = DrawNodeHeader (y);

            Rect buttonRect = new Rect (rect.xMax - Styles.headerButtonPaddingRight - Styles.headerButtonSize.x, rect.center.y - Styles.headerButtonSize.y / 2, Styles.headerButtonSize.x, Styles.headerButtonSize.y);

            int id = GUIUtility.GetControlID ("AI.Graph.Editor.NodeView.HeaderToggle".GetHashCode (), FocusType.Passive, buttonRect);
            switch (Event.current.GetTypeForControl (id)) {
                case EventType.MouseDown:

                    if (Event.current.button == 0 && Event.current.IsMouseInsideRect (buttonRect)) {
                        GUIUtility.keyboardControl = id;
                        GUIUtility.hotControl = id;

                        isCollapsed = !isCollapsed;
                        Event.current.Use ();
                    }

                    break;

                case EventType.MouseUp:
                    if (GUIUtility.hotControl == id && Event.current.button == 0) {
                        GUIUtility.hotControl = 0;
                        GUIUtility.keyboardControl = 0;

                        Event.current.Use ();
                    }

                    break;

                case EventType.Repaint:

                    if (node.rect.Contains (mousePositionOnNode)) {
                        if (Event.current.IsMouseInsideRect (buttonRect)) {
                            EditorGUI.DrawSolidTexture (buttonRect, colors.primaryColor2, Styles.headerButtonBorderRadius);
                        }

                        Rect buttonIconRect = new Rect (buttonRect.center - Styles.headerButtonIconSize / 2, Styles.headerButtonIconSize);

                        if (isCollapsed) {
                            EditorGUI.DrawTexture (buttonIconRect, Styles.collabsibleHeaderButtonIconOff, ScaleMode.ScaleToFit, colors.primaryColor1);
                        }
                        else {
                            EditorGUI.DrawTexture (buttonIconRect, Styles.collabsibleHeaderButtonIconOn, ScaleMode.ScaleToFit, colors.primaryColor1);
                        }
                    }

                    break;
            }

            return rect;
        }


        /// <summary>
        /// Draws an active animation rect.
        /// </summary>
        /// <param name="y">The start point.</param>
        /// <param name="animationValue">Value for the animation state.</param>
        /// <returns>Returns the size of the draw area.</returns>
        protected internal Rect DrawActiveStateAnimation (float y, bool active, ref float animationValue) {
            if (active) {
                animationValue += Styles.activeStateAnimationSpeed * canvas.deltaTime;
                if (animationValue >= 1) animationValue = 0;
            }
            else {
                animationValue = 0;
            }

            Rect rect = new Rect (0, y, node.size.x, Styles.progressHeight);

            if (Event.current.IsRepaintEvent ()) {
                EditorGUI.DrawSolidTexture (rect, colors.primaryColor2);

                if (active) {
                    float x = rect.width * animationValue;
                    Rect progressFillRect = new Rect (x, y, Styles.activeStateAnimationRectWidth, Styles.progressHeight);
                    EditorGUI.DrawSolidTexture (progressFillRect, colors.primaryColor1);
                }

                EditorGUI.DrawSolidTexture (rect, Styles.outlineColor.color, new BorderWidth (Styles.outlineWidth, Styles.outlineWidth, 0, 0));
            }

            return rect;
        }


        /// <summary>
        /// Draws a area with the given ports.
        /// The inputs will be drawn on the left side.
        /// The outputs will be drawn on the right side.
        /// </summary>
        /// <param name="y">The start point.</param>
        /// <param name="inputs">All inputs that should be drawn. If this is null, nothing will be drawn.</param>
        /// <param name="outputs">All outputs that should be drawn. If this is null, nothing will be drawn.</param>
        /// <returns>Returns the size of the draw area.</returns>
        protected internal Rect DrawNodePortArea (float y, ConnectionPort[] inputs, ConnectionPort[] outputs) {
            float maxPortCount = Mathf.Max (inputs != null ? inputs.Length : 0, outputs != null ? outputs.Length : 0);
            float connectionPortsAreaHeight = (maxPortCount * Styles.visualPortSize.y) + Mathf.Max (maxPortCount - 1, 0) * Styles.connectionPortSpacing + Styles.connectionPortPaddingTop + Styles.connectionPortPaddingBottom;

            Rect connectionPortsAreaRect = new Rect (0, y, node.size.x, connectionPortsAreaHeight);

            EditorGUI.DrawSolidTexture (connectionPortsAreaRect, Styles.background100.color);

            if (inputs != null) {
                for (int i = 0; i < inputs.Length; i++) {
                    Vector2 position = new Vector2 (
                        node.position.x + Styles.connectionPortPaddingLeft,
                        node.position.y + y + Styles.connectionPortPaddingTop + Mathf.Max (i, 0) * (Styles.connectionPortSpacing + Styles.visualPortSize.y)
                    );

                    DrawConnectionPort (inputs[i], position, Alignment.TopLeft, LabelPosition.Right);
                }
            }

            if (outputs != null) {
                for (int i = 0; i < outputs.Length; i++) {
                    Vector2 position = new Vector2 (
                        node.position.x + node.size.x - Styles.connectionPortPaddingRight,
                        node.position.y + y + Styles.connectionPortPaddingTop + Mathf.Max (i, 0) * (Styles.connectionPortSpacing + Styles.visualPortSize.y)
                    );

                    DrawConnectionPort (outputs[i], position, Alignment.TopRight, LabelPosition.Left);
                }
            }

            EditorGUI.DrawSolidTexture (connectionPortsAreaRect, Styles.outlineColor.color, new BorderWidth (Styles.outlineWidth, Styles.outlineWidth, Styles.outlineWidth, 0));

            return connectionPortsAreaRect;
        }


        /// <summary>
        /// Draws a connection port and updates the position.
        /// </summary>
        /// <param name="port">The port that should be drawn.</param>
        /// <param name="newPortPosition">The new position of the port.</param>
        /// <param name="portPositionAlignment">The alignment of the draw position.</param>
        /// <param name="labelPosition">The label position.</param>
        /// <param name="updateOnlyPosition">If this is true, only the position of the given port will be updated.</param>
        protected internal void DrawConnectionPort (ConnectionPort port, Vector2 newPortPosition, Alignment portPositionAlignment, LabelPosition labelPosition, bool updateOnlyPosition = false) {
            Vector2 boundsPosition = Vector2.zero;

            switch (portPositionAlignment) {
                case Alignment.TopLeft:
                    boundsPosition = newPortPosition + Styles.visualPortSize / 2 - Styles.connectionPortBoundsSize / 2;
                    break;
                case Alignment.TopRight:
                    boundsPosition = new Vector2 (newPortPosition.x - Styles.visualPortSize.x / 2 - Styles.connectionPortBoundsSize.x / 2, newPortPosition.y + Styles.visualPortSize.y / 2 - Styles.connectionPortBoundsSize.y / 2);
                    break;
                case Alignment.BottomLeft:
                    boundsPosition = new Vector2 (newPortPosition.x + Styles.visualPortSize.x / 2 - Styles.connectionPortBoundsSize.x / 2, newPortPosition.y - Styles.visualPortSize.y / 2 - Styles.connectionPortBoundsSize.y / 2);
                    break;
                case Alignment.BottomRight:
                    boundsPosition = newPortPosition - Styles.visualPortSize / 2 - Styles.connectionPortBoundsSize / 2;
                    break;
                case Alignment.Center:
                    boundsPosition = newPortPosition - Styles.connectionPortBoundsSize / 2;
                    break;
                case Alignment.MiddleLeft:
                    boundsPosition = new Vector2 (newPortPosition.x + Styles.visualPortSize.x / 2 - Styles.connectionPortBoundsSize.x / 2, newPortPosition.y - Styles.connectionPortBoundsSize.y / 2);
                    break;
                case Alignment.MiddleRight:
                    boundsPosition = new Vector2 (newPortPosition.x - Styles.visualPortSize.x / 2 - Styles.connectionPortBoundsSize.x / 2, newPortPosition.y - Styles.connectionPortBoundsSize.y / 2);
                    break;
                case Alignment.MiddleTop:
                    boundsPosition = new Vector2 (newPortPosition.x - Styles.connectionPortBoundsSize.x / 2, newPortPosition.y + Styles.visualPortSize.y / 2 - Styles.connectionPortBoundsSize.y / 2);
                    break;
                case Alignment.MiddleBottom:
                    boundsPosition = new Vector2 (newPortPosition.x - Styles.connectionPortBoundsSize.x / 2, newPortPosition.y - Styles.visualPortSize.y / 2 - Styles.connectionPortBoundsSize.y / 2);
                    break;
            }

            port.position = boundsPosition;
            port.size = Styles.connectionPortBoundsSize;


            if (!updateOnlyPosition) {
                Vector2 visualPosition = boundsPosition - node.position + Styles.connectionPortBoundsSize / 2 - Styles.visualPortSize / 2;
                Rect connectionPortRect = new Rect (visualPosition, Styles.visualPortSize);

                Color portColor = colors.primaryColor1;

                EditorGUI.DrawSolidTexture (connectionPortRect, portColor, new BorderWidth (Styles.connectionPortStroke), new BorderRadius (90));

                if (port.hasConnections || canvas.IsPortSelected (port) || port.rect.Contains (mousePositionOnNode)) {
                    EditorGUI.DrawTexture (connectionPortRect, Styles.portHoverIcon, ScaleMode.ScaleToFit, 0, portColor, true, BorderWidth.zero, BorderRadius.zero);
                }

                switch (labelPosition) {
                    case LabelPosition.Left:
                        EditorGUI.Label (
                            visualPosition.x - Styles.connectionPortLabelPadding - Styles.portLabelWidth, visualPosition.y,
                            Styles.portLabelWidth, Styles.visualPortSize.y, port.name, Styles.portLabelStyleLeft
                        );
                        break;
                    case LabelPosition.Right:
                        EditorGUI.Label (
                            visualPosition.x + Styles.visualPortSize.x + Styles.connectionPortLabelPadding, visualPosition.y,
                            Styles.portLabelWidth, Styles.visualPortSize.y, port.name, Styles.portLabelStyleRight
                        );
                        break;
                    case LabelPosition.Top:
                        EditorGUI.Label (
                            visualPosition.x + Styles.visualPortSize.x / 2 - Styles.portLabelWidth / 2, visualPosition.y - Styles.connectionPortLabelPadding - Styles.visualPortSize.y,
                            Styles.portLabelWidth, Styles.visualPortSize.y, port.name, Styles.portLabelStyleTop);
                        break;
                    case LabelPosition.Bottom:
                        EditorGUI.Label (
                            visualPosition.x + Styles.visualPortSize.x / 2 - Styles.portLabelWidth / 2, visualPosition.y + Styles.connectionPortLabelPadding + Styles.visualPortSize.y,
                            Styles.portLabelWidth, Styles.visualPortSize.y, port.name, Styles.portLabelStyleBottom);
                        break;
                }
            }
        }


        /// <summary>
        /// Draw a blank space area.
        /// </summary>
        /// <param name="y">The start point.</param>
        /// <param name="height">The height of the blank area</param>
        /// <returns>Returns the size of the draw area.</returns>
        protected internal Rect DrawSpaceArea (float y, float height) {
            Rect rect = new Rect (0, y, node.size.x, height);

            if (Event.current.IsRepaintEvent ()) {
                EditorGUI.DrawSolidTexture (rect, Styles.background100.color);
                EditorGUI.DrawSolidTexture (rect, Styles.outlineColor.color, new BorderWidth (Styles.outlineWidth, Styles.outlineWidth, 0, 0));
            }

            return rect;
        }


        /// <summary>
        /// Draws a node footer.
        /// </summary>
        /// <param name="y">The start point.</param>
        /// <returns>Returns the size of the draw area.</returns>
        protected internal Rect DrawNodeFooter (float y) {
            Rect rect = new Rect (0, y, node.size.x, Styles.footerHeight);

            if (Event.current.IsRepaintEvent ()) {
                EditorGUI.DrawSolidTexture (rect, Styles.background200.color, new BorderRadius (0, 0, Styles.nodeBorderRadius.bottomLeft, Styles.nodeBorderRadius.bottomRight));
                EditorGUI.DrawSolidTexture (rect, Styles.outlineColor.color, new BorderWidth (Styles.outlineWidth), new BorderRadius (0, 0, Styles.nodeBorderRadius.bottomLeft, Styles.nodeBorderRadius.bottomRight));
            }

            return rect;
        }


        protected internal static class Styles {

            public static EditorColor background100 = new EditorColor (new Color32 (190, 190, 190, 200), new Color32 (44, 44, 44, 200));
            public static EditorColor background200 = new EditorColor (new Color32 (140, 140, 140, 255), new Color32 (34, 34, 34, 255));
            public static EditorColor background300 = new EditorColor (new Color32 (120, 120, 120, 200), new Color32 (24, 24, 24, 200));
            public static EditorColor outlineColor = new EditorColor (new Color32 (40, 40, 40, 255), new Color32 (10, 10, 10, 255));
            public static EditorColor portLabelColor = new EditorColor (new Color32 (0, 0, 0, 255), new Color32 (255, 255, 255, 255));

            public static float activeStateAnimationRectWidth = 20;
            public static float activeStateAnimationSpeed = 0.6F;
            public static float collapseAnimationSpeed = 4F;

            public static readonly float portLabelWidth = 75;

            public static float outlinePadding = 5;
            public static BorderWidth outlineBorderWidth = new BorderWidth (2);
            public static BorderRadius outlineBorderRadius = new BorderRadius (6);

            public static readonly Texture2D portHoverIcon;
            public static readonly Texture2D collabsibleHeaderButtonIconOn;
            public static readonly Texture2D collabsibleHeaderButtonIconOff;

            public static readonly Vector2 visualPortSize = new Vector2 (9, 9);

            public static readonly GUIStyle portLabelStyleLeft;
            public static readonly GUIStyle portLabelStyleTop;
            public static readonly GUIStyle portLabelStyleRight;
            public static readonly GUIStyle portLabelStyleBottom;
            public static readonly GUIStyle nodeLabelStyle;

            public static float headerHeight = 30;
            public static float nodeLabelPaddingLeft = 12;
            public static float headerButtonPaddingRight = 8;
            public static Vector2 headerButtonSize = new Vector2 (20, 20);
            public static Vector2 headerButtonIconSize = new Vector2 (20, 20);
            public static BorderRadius headerButtonBorderRadius = new BorderRadius (4);
            public static BorderRadius nodeBorderRadius = new BorderRadius (4);

            public static float progressHeight = 5;

            public static float connectionPortPaddingTop = 8;
            public static float connectionPortPaddingBottom = 8;
            public static float connectionPortPaddingLeft = 8;
            public static float connectionPortPaddingRight = 8;
            public static float connectionPortSpacing = 10;
            public static float connectionPortLabelPadding = 5;
            public static float connectionPortStroke = 1;
            public static Vector2 connectionPortBoundsSize = new Vector2 (17, 15);

            public static float collapseAreaPaddingTop = 10;
            public static float collapseAreaPaddingBottom = 10;
            public static float collapseAreaPaddingLeft = 10;
            public static float collapseAreaPaddingRight = 10;

            public static float footerHeight = 16;

            public static float outlineWidth = 1;

            public static readonly Font nodeFont;


            static Styles () {
                nodeFont = EditorGUIUtility.LoadAsset<Font> ("Fonts/OpenSans-Regular.ttf");

                portLabelStyleLeft = new GUIStyle ();
                portLabelStyleLeft.alignment = TextAnchor.MiddleRight;
                portLabelStyleLeft.fontSize = 10;
                portLabelStyleLeft.font = nodeFont;

                portLabelStyleTop = new GUIStyle ();
                portLabelStyleTop.alignment = TextAnchor.MiddleCenter;
                portLabelStyleTop.fontSize = 10;
                portLabelStyleTop.font = nodeFont;

                portLabelStyleRight = new GUIStyle ();
                portLabelStyleRight.alignment = TextAnchor.MiddleLeft;
                portLabelStyleRight.fontSize = 10;
                portLabelStyleRight.font = nodeFont;

                portLabelStyleBottom = new GUIStyle ();
                portLabelStyleBottom.alignment = TextAnchor.MiddleCenter;
                portLabelStyleBottom.fontSize = 10;
                portLabelStyleBottom.font = nodeFont;

                nodeLabelStyle = new GUIStyle ();
                nodeLabelStyle.normal.textColor = Color.white;
                nodeLabelStyle.alignment = TextAnchor.MiddleLeft;
                nodeLabelStyle.fontSize = 14;
                nodeLabelStyle.font = nodeFont;

                portHoverIcon = EditorGUIUtility.LoadAsset<Texture2D> ("Textures/connection_port_connected.png");
                collabsibleHeaderButtonIconOn = EditorGUIUtility.LoadAsset<Texture2D> ("Textures/btn_burger.png");
                collabsibleHeaderButtonIconOff = EditorGUIUtility.LoadAsset<Texture2D> ("Textures/btn_burger_close.png");

                SetGUIStyleColors ();

                AIGraphPreferences.onSkinChanged += skin => { SetGUIStyleColors (); };
            }


            static void SetGUIStyleColors () {
                portLabelStyleLeft.normal.textColor = portLabelColor.color;
                portLabelStyleTop.normal.textColor = portLabelColor.color;
                portLabelStyleRight.normal.textColor = portLabelColor.color;
                portLabelStyleBottom.normal.textColor = portLabelColor.color;
            }

        }

    }


}
#endif